← 返回文章列表

从零构建 AI Agent (一):Context Engineering 与 Harness Engineering 实战手记

三天时间,从一个 10 行的 API 调用,到一个具备自我审查与纠正能力的模块化 Agent 系统。这篇文章记录了完整的学习路径、踩过的每一个坑、以及由此提炼出的工程认知。

agent
harness

三天时间,从一个 10 行的 API 调用,到一个具备自我审查与纠正能力的模块化 Agent 系统。这篇文章记录了完整的学习路径、踩过的每一个坑、以及由此提炼出的工程认知。

作者: Jinkun
时间: 2026年4月
项目地址: github.com/yaoziyaoguai/my-first-agent
技术栈: Python 3.12 · Anthropic SDK(兼容协议)· Kimi-k2.5 · Qwen3-max


一、为什么写这篇文章

随着 AI Agent 从“能跑”走向“能稳定落地”,工程实践中逐渐形成了两条重要的方法论脉络:其一是 Context Engineering(参考 ),关注如何为模型组织、筛选与注入高质量上下文;其二是 Harness Engineering,关注如何通过外部工程结构来约束、验证并增强 Agent 的行为可靠性。前者并非在 2026 年才出现,而是在 2025 年已被持续推广,并由 Anthropic 进一步系统化;后者则在 2026 年由 ThoughtWorks 的 Birgitta Böckeler 在 Martin Fowler 网站上得到更完整的阐述。 读完这些文章后我有一个强烈的感觉:光看概念远远不够,必须动手构建才能真正理解。于是我决定从零开始,不用任何 Agent 框架,只用 Python + Anthropic SDK,一步一步构建一个完整的 Agent,在过程中体会这两个框架的每一个设计决策。


二、核心概念:先搞清楚我们在造什么

2.1 Agent 的本质

在动手之前,需要先破除一个误解:Agent 不是一个更聪明的聊天机器人。

大模型(LLM)本身是完全无状态的。每次调用都是一次全新的推理——它不记得你是谁,不记得上一轮说了什么。所谓的"记忆"完全是外部代码维护的一个消息列表(messages),每次调用时完整发送给模型,模型读一遍,产生回复,然后就忘了。

同样,模型也从未"真正"调用过任何工具。它只是生成了一段结构化文本说"我想调用 calculate,参数是 123 * 456",然后就停了。是我们写的代码在中间做了所有实际工作——解析请求、执行计算、把结果包装成特定格式塞回去。模型下次被调用时看到结果,它的训练让它"理解"这个对话模式。

这意味着 Agent 的"自主性"是两层配合的结果:模型提供决策能力,外部代码提供循环机制。 缺了任何一层都不行。

Agent 的核心公式:

Agent = Model + Harness

2.2 Context Engineering:管模型"看到"什么

Anthropic 的定义:优化送入 LLM 的 token 的效用,在模型固有的约束下,持续达成预期结果。

简单说就是:在每一个决策时刻,应该把哪些信息放进模型的上下文窗口?

它面对三个经典挑战:

  • Lost in the Middle:上下文过长时,模型对中间部分的注意力下降,倾向于关注开头和结尾
  • 信息缺失:关键信息未被送入上下文,模型只能猜测,产生幻觉
  • 窗口溢出:历史累积超出容量,早期信息被丢失

2.3 Harness Engineering:让 Agent 可靠运行

Harness 是"模型之外的一切"。它的控制体系有两个维度:

按方向分:

  • Guides(前馈控制):行动之前预防问题
  • Sensors(反馈控制):行动之后检测并纠正

按材质分:

  • Computational(计算型):确定性的、快速的(如白名单、类型检查、Linter)
  • Inferential(推理型):非确定性的、基于 LLM 的(如 AI Code Review)

Harness 要调节三个维度:可维护性(工具最成熟)、架构适应性(中等成熟)、行为正确性(最难,原文称之为"房间里的大象")。

2.4 两者的关系

这个问题我纠结了很久。一开始我以为是包含关系——Context Engineering 被 Harness Engineering 包含。后来反复讨论,发现更准确的理解是:

Context Engineering 是底层管道——负责把信息送进模型。 Harness Engineering 是上层策略——用这个管道来构建完整的控制体系。

同一个组件往往同时扮演两个角色。比如 CLAUDE.md 文件,从 Context Engineering 角度看是"送进模型的信息",从 Harness 角度看是"一个前馈控制器"。在实践中它们交织在一起,不需要强行划分边界。


三、渐进式构建:从 10 行到 800 行

3.1 演进路径总览

阶段文件核心突破代码量
101_single_call.pyAPI 能通了~10 行
202_agent_loop.py有记忆了(messages 列表)~30 行
303_agent_with_tool.py能动手了(工具调用 + Agent Loop)~80 行
404_agent_with_harness.py有约束了(日志 + 确认 + 白名单)~150 行
505_agent_with_files.py完整系统~800 行
6模块化重构拆分为 agent/ 包~800 行(7 个文件)

3.2 阶段一:从沉默到说话

第一个文件只做一件事——调用 API,确认能通。

response = client.messages.create(
    model="kimi-k2.5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "你好,请用一句话介绍你自己"}
    ],
)
print(response.content[0].text)

第一个坑就来了:模型返回的第一个 content block 不是文字,而是一个 ThinkingBlock(Kimi 的深度思考模式)。这让我学到了第一课:不同模型的返回格式可能不同,不能假设 content[0] 就是文字。

3.3 阶段二:从失忆到记忆

第二版加了一个 messages 列表和一个 while True 循环。每轮对话的用户输入和模型回复都追加到列表里,下次调用时完整发送。

关键认知:这个 messages 列表就是 Context Engineering 的核心对象。 后面所有的上下文管理——压缩、截断、摘要——都是在操作这个列表。

3.4 阶段三:从说话到动手

给 Agent 加了一个计算器工具。这一步的概念跨越最大——模型的回复不再只是文字,它可能是一个"我想调用工具"的请求。代码需要检测这个请求、执行工具、把结果塞回去、再次调用模型。

此时代码里出现了两个循环:外层循环处理用户交互,内层循环处理工具调用。内层循环就是真正的 Agent Loop。

一个深刻的感悟来自工具描述的实验:如果把计算器的 description 从"计算数学表达式"改成"翻译英文",模型就不会在用户问数学题时调用它。工具描述是模型理解工具的唯一依据。

3.5 阶段四:从裸奔到穿甲

这一步加了三个 Harness 机制: 这三个机制的选择并非来自某个特定的论文或框架,而是基于三个基本工程原则:可观测性(你无法改进你看不见的东西)、最小权限(不在白名单里的一律拒绝)、以及人在回路(Human-in-the-loop,关键决策保留人类判断)。后续的所有 Harness 机制——权限分级、源码保护、跨模型审查——都不是预先设计的,而是在实际使用中遇到问题后逐步"长出来"的。这正好印证了 Harness Engineering 的核心理念:Steering Loop 是一个持续迭代的过程。

可观测性 → 日志(你得看见发生了什么)

可控制性 → 确认(你得能拦住危险操作)

可预测性 → 白名单(你得限定行为边界)

日志系统(Sensor,计算型)——记录每一步决策。后来的每一次调试都依赖这个日志。教训:你无法改进你看不见的东西。

执行确认(Guide,计算型)——工具执行前让人类确认。当时只有计算器觉得多余,后来加了文件写入才体会到它的救命价值。

工具白名单(Guide,计算型)——防止模型幻觉出不存在的工具。

还加了 Session 快照功能,遇到了第一个工程问题:Anthropic SDK 返回的是 Python 对象,不是普通字典,json.dump 无法序列化。解决方法是写了一个 make_serializable 函数递归转换。Agent 跟外部世界交互时,数据格式的转换是常见的工程细节。

3.6 阶段五:完整系统的诞生(与无数的坑)

这个阶段是密度最高的,几乎每一步都踩出了新问题。

权限分级的设计

给 Agent 加了文件读写能力后,一个核心问题浮出水面:读文件和写文件的危险等级不同,应该施加不同的控制。

我设计的分级规则:

  • 读项目内文件 → 静默执行
  • 读项目外文件 → 需确认
  • 写任何文件 → 需确认

设计原则是:控制强度与操作风险成正比。 风险由两个因素决定——可逆性和影响范围。

上下文膨胀的三次搏斗

第一次:按条数压缩。 当 messages 超过 10 条时,把旧消息用 LLM 总结成摘要。结果发现压缩后 Agent 跑偏了——关键信息(文件名、路径)被摘要丢掉了。

第二次:压缩前先截断 tool_result。 在让 LLM 总结之前,先把 tool_result 里的大块内容截断成摘要级别。两阶段策略:"先降噪,再总结"。效果好了很多。

第三次:按字节数触发。 Agent 读了一个大文件,只有 5 条消息但总内容量巨大,直接触发了 API 的 Backend buffer overflow。原来只按条数判断是不够的,加了字节数作为第二个触发条件。

源码保护的攻防

Agent 在被要求重构代码时,试图修改自己的 .py 源文件。这是一个 Harness 问题——Agent 不应该修改自己的代码。 加了源码保护后,又发现太严格了——Agent 想在 workspace 下创建新的 .py 文件也被拦住了。最终的规则是:只保护已存在的 .py 文件,新文件可以创建但需要确认。

流式输出的体验优化

messages.create() 会等模型把整个回复生成完才返回,十几秒的等待中什么都看不到。改用 messages.stream() 后文字一个字一个字出现。但 tool_use 阶段没有文字输出,用户又觉得卡了——于是加了"🔧 正在规划工具调用..."的提示。

流式输出不是 Context Engineering(没改变模型看到的信息),而是 Harness 的一部分——实时可见性本身就是一种 Sensor。

最痛苦的 Bug:缩进导致的逻辑错误

在处理工具执行结果的代码中,else 分支对齐到了 if tool_name == "write_file" 而非 if approved。结果:所有非写文件的工具(包括 read_file)在执行成功后,返回值被覆盖为"用户拒绝了此操作"。

模型收到这个假的拒绝信息后,以为读取被拒绝了,反复用不同路径重试。我花了很长时间才定位到这个问题——因为日志里记录的是真实的执行结果(成功),而模型看到的是被覆盖后的结果(拒绝)。Python 的缩进就是逻辑,差一层意思完全不同。

这也是我最终决定做架构重构的直接原因——800 行单文件里的五层嵌套 if/else,太容易出这种错误了。

3.7 跨模型审查系统

为什么不能自己审查自己

最初的审查是用同一个模型来评判自己的输出,结果全部满分。原因是:同一个模型的生成标准和评判标准是同一套权重,它天然觉得自己的输出"不错"。

改成用不同模型做审查(Agent 用 Kimi-k2.5,审查用 Qwen3-max)后,评分更加严格客观。

四次迭代

  1. 只传回复文字 → 审查模型只听 Agent 自说自话,缺乏判断依据
  2. 加入工具调用记录 → 审查模型能对照实际数据,提到了具体版本号
  3. 修复 JSON 解析 → 处理模型返回 markdown 代码块包裹的 JSON
  4. 加入自动重试 → 审查不通过时将反馈注入 messages,Agent 自动修正

审查的局限性

审查模型给了项目介绍文件 5 分满分,但文件里说这是"基于 Anthropic Claude API 的项目"——实际上我用的是 Kimi。审查模型看到 requirements.txt 里有 anthropic 包就信了。推理型 Sensor 只能基于它看到的信息做判断,有些需要更深层上下文的问题只有人类能发现。

3.8 安全加固:从 eval 到 AST

calculate 函数用的 eval() 是一个严重的安全隐患——虽然有字符白名单,但 eval 的攻击面太大,靠黑名单永远堵不完。

安全领域的原则:不要试图过滤危险输入,而是只允许安全的操作。

用 Python 的 ast 模块把表达式解析成语法树,只允许数字节点和运算符节点通过。任何函数调用、属性访问、列表推导等语法结构都会被拒绝。这是从"字符层面的白名单"升级到"语法层面的白名单"。

有趣的是,Agent 虽然被 calculate_safe 拦住了 __import__('os').listdir('.'),但它"聪明"地绕了一条路——试图写一个 Python 脚本来完成同样的操作。这就是原文说的 Behaviour Harness 是最难的——你堵住了工具层面的漏洞,Agent 在行为层面找到了绕过方式。好在多层防护(写文件需要确认)最终拦住了它。

3.9 架构重构:提升 Harnessability

800 行单文件的痛点很明显:每次改一个小问题都要把整个逻辑看很多次,改到哪里了也不知道。缩进 bug 就是最好的例证。

Harness 原文有一个概念——Harnessability(可驾驭性):不是所有代码库都同样容易加 Harness。好的架构本身就让 Harness 更容易加。

重构后的结构:

my-first-agent/
├── config.py              ← 所有配置集中管理
├── main.py                ← 入口,20 行
├── agent/
│   ├── __init__.py
│   ├── core.py            ← Agent Loop 流程控制
│   ├── tools.py           ← 工具实现 + 工具描述
│   ├── security.py        ← 权限分级 + 源码保护
│   ├── context.py         ← 上下文压缩
│   ├── review.py          ← 跨模型审查 + 自动重试
│   └── logger.py          ← 日志 + 快照
└── workspace/

重构中的一个关键设计决策:compress_history 和 review_agent_output 从修改全局变量改为接收参数、返回结果。这消除了对全局状态的依赖,也避免了模块之间的循环导入。

SESSION_ID 的归属也引发了思考——它不是配置(不是静态的),跟对话最相关但如果放在 core.py 会导致跟 logger.py 的循环依赖。最终放在 logger.py 里,因为它是日志系统的一部分。


四、完整的 Harness 体系

Guides(前馈控制)

Guide材质作用
工具白名单计算型防止模型幻觉出不存在的工具
权限分级系统计算型按操作类型 × 路径位置决定是否需要确认
源码保护计算型禁止修改项目目录下已存在的 .py 文件
单轮单写限制计算型同一轮响应中只允许一次 write_file
AST 安全计算计算型只允许数学运算节点,拒绝一切函数调用和属性访问
System Prompt 规则推理型要求逐文件创建,不一次性完成
Tool Result 停止指令推理型写文件成功后强制模型停下来

Sensors(反馈控制)

Sensor材质作用
JSONL 日志系统计算型记录每一步决策
Session 快照计算型完整消息历史的持久化
写入前自动备份计算型确保可逆性
工具错误捕获计算型返回有意义的错误信息供模型自纠正
流式输出计算型实时可见性
跨模型质量审查推理型用 Qwen3-max 审查 Kimi 的输出
审查驱动自动重试推理型不通过时自动将反馈注入上下文重试

Context Engineering 实践

技术解决的问题
System Prompt 设计Agent 身份和行为规则
工具描述精确性模型对工具能力的理解
大文件分段读取防止大文件撑爆上下文
多格式结构提取Python/Markdown/JSON/YAML/SQL/JS 目录
双重触发压缩条数 + 字节数
两阶段压缩先截断 tool_result,再 LLM 总结
Tool Result 信息注入把控制指令放在上下文最新位置

五、踩坑清单

问题根因解决方案分类
ThinkingBlock 不是文字模型返回格式差异遍历 content 找 text 类型兼容性
SDK 对象无法序列化不是普通字典model_dump() 递归转换工程细节
审查 JSON 解析失败模型返回 markdown 代码块剥掉 ``` 标记推理型 Sensor
审查结果被淹没流式输出 + 主循环重复打印去掉重复打印可观测性
大文件概览后反复重试返回信息缺乏"成功"标识改措辞 + 改工具描述Context Engineering
5 条消息触发 buffer overflow只按条数压缩加字节数触发条件Context Engineering
缩进导致 result 被覆盖if/else 对齐错误修正缩进 + 架构重构工程质量
Agent 绕过工具限制改用写脚本方式多层防护叠加Behaviour Harness
模块拆分后循环依赖SESSION_ID 归属问题放在 logger.py架构设计

六、核心认知

6.1 模型是无状态的函数

每次调用都是全新推理。"记忆"是 messages 列表,"工具使用"是结构化文本的生成与解析,"循环"是外部代码的 while True。理解了这一点,才能理解为什么 Context Engineering 如此重要——你喂什么,它就产出什么。

6.2 Harness 是长出来的,不是设计出来的

没有一个 Guide 或 Sensor 是我在第一天就能预见到的。每一个都源自一个真实的失败场景。这就是 Steering Loop 的精髓——发现问题、改进 Harness、继续运行、发现新问题。

6.3 控制强度应与风险成正比

不是所有操作都需要同等控制。计算型控制便宜可靠优先用,推理型控制昂贵但能捕捉语义问题选择性部署。风险越高,控制层数越多。

6.4 好的架构本身就是 Harness

800 行单文件里的缩进 bug 证明了这一点。重构成模块化之后,每个文件的职责清晰,加新的 Guide 和 Sensor 更容易,也更不容易引入 bug。Harnessability 是一个值得从第一天就考虑的属性。

6.5 同一个组件,两个视角

System Prompt 既是 Context Engineering(送进模型的信息)又是 Guide(行为约束)。Tool Result 中的停止指令既是 Context Engineering(注入到上下文)又是 Guide(控制模型行为)。不需要纠结分类,理解它在两个维度上分别起什么作用就够了。

6.6 推理型控制的天花板

同模型审查自信偏高。跨模型审查更客观但仍有盲点——它只能基于看到的信息判断。有些需要深层业务上下文的问题,只有人类能发现。Harness 的目标不是消除人类参与,而是把人类的注意力引导到最需要的地方。


七、与产品级 Agent 的差距

让 Agent 自己分析自己(经人工验证)的结果:

维度完成度最关键的缺失
架构设计██████░░░░ 60%已完成模块化,但缺少插件化和中间件
工具生态████░░░░░░ 40%缺少代码执行、网络请求、搜索
安全体系████░░░░░░ 40%无沙箱、无路径遍历防护
智能规划███░░░░░░░ 30%缺少多步骤规划、长期记忆
性能可靠██░░░░░░░░ 25%单用户、无限流、无容错
用户体验██░░░░░░░░ 20%缺少多模态、中断恢复
开发运维█░░░░░░░░░ 15%无测试、无 CI/CD

八、最终系统架构

Agent System
│
├── config.py                    ← 配置中心
├── main.py                      ← 入口(20 行)
│
├── agent/
│   ├── core.py                  ← Agent Loop 流程控制
│   │   ├── 流式输出
│   │   ├── tool_use 处理循环
│   │   └── 审查驱动的自纠正循环
│   │
│   ├── tools.py                 ← 工具层
│   │   ├── calculate(AST 安全版)
│   │   ├── read_file(概览 + 多格式结构提取)
│   │   ├── read_file_lines(按行读取)
│   │   ├── write_file(带备份)
│   │   ├── execute_tool(分发器)
│   │   └── TOOL_DEFINITIONS(工具描述)
│   │
│   ├── security.py              ← 安全层
│   │   ├── is_protected_source_file
│   │   ├── needs_confirmation(分级规则)
│   │   └── confirm_tool_call
│   │
│   ├── context.py               ← 上下文管理
│   │   ├── estimate_messages_size
│   │   ├── _truncate_tool_result_content
│   │   └── compress_history(双重触发 + 两阶段)
│   │
│   ├── review.py                ← 审查系统
│   │   ├── review_agent_output(跨模型)
│   │   ├── should_review_turn(选择性审查)
│   │   ├── build_retry_feedback
│   │   └── get_effective_review_request
│   │
│   └── logger.py                ← 可观测性
│       ├── log_event(JSONL)
│       ├── save_session_snapshot
│       └── make_serializable
│
└── 01-05_*.py                   ← 学习历程记录

九、下一步

  1. 敏感文件保护:防止 Agent 读取 .env 等包含密钥的文件
  2. Shell 命令执行:风险最高的工具,需要叠加黑名单 + 确认 + 超时 + 日志
  3. 时序分布:把 Sensor 扩展到 CI/CD 流水线(提交前检查 + 持续漂移检测)
  4. 工具插件化:让添加新工具变成"注册"而非"修改代码"

十、写在最后

三天时间,从零开始构建了一个有记忆、有工具、有分级控制、有上下文管理、有跨模型审查、有自动纠正的 Agent 系统。过程中踩了无数的坑——缩进 bug、序列化问题、上下文溢出、审查结果被淹没、模型绕过工具限制……

但我最深刻的收获不是这些技术细节,而是一个认知:

Agent 开发不是一次性的设计,而是持续的 Steering Loop。 你写了代码 → 跑出了问题 → 加了一个 Guide 或 Sensor → 又跑出了新问题 → 再迭代。你的 Harness 永远不会"完成",只会越来越健壮。

如果你也想从零开始学 Agent 开发,我的建议是:不要用框架,自己手写 Agent Loop。 只有亲手管理 messages 列表、亲手处理 tool_use 的请求和返回、亲手遇到上下文溢出的问题,你才能真正理解这些框架在帮你做什么。


本文的所有代码均为作者亲手编写和调试。学习过程中与 Claude 协作,采用苏格拉底式教学——Claude 提问引导,作者思考并实现。

分享这篇文章

复制链接,或分享到你常用的地方。

评论

发表评论

0 / 1000

KEEP READING

全部文章